using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Text;
using System.Xml;
using sprocgen.Entities;

namespace sprocgen
{
    class Program
    {
        const string GetStoredProcedureDataSql = @"select 
so.id [StoredProcedureID]
, su.name [StoredProcedureSchema]
, so.name [StoredProcedureName]
, sc.colid [ParameterID]
, sc.name [ParameterName]
, st.name [ParameterTypeName]
, sc.length [ParameterLength]
--, sp.has_default_value [ParameterHasDefaultValue] /* doesn't work */
, sc.isoutparam [ParameterIsOutputParameter]
from sysobjects so
join sysusers su
on su.uid = so.uid
left join syscolumns sc
on so.id = sc.id
left join systypes st
on st.xtype = sc.xtype
where so.xtype = 'p'
and st.name != 'sysname'
order by so.name, sc.colid
";

        private static string GetString(object value)
        {
            if (DBNull.Value.Equals(value))
                return null;
            return (string)value;
        }

        private static int GetInt32(object value)
        {
            return (int)value;
        }

        private static int? GetInt32Nullable(object value)
        {
            if (DBNull.Value.Equals(value))
                return null;
            return int.Parse(string.Format("{0}", value));
        }

        private static bool? GetBooleanNullable(object value)
        {
            if (DBNull.Value.Equals(value))
                return null;
            return (bool)value;
        }

        static int Main(string[] args)
        {
            try
            {
                if (args == null || args.Length == 0)
                {
                    CreateConfig();
                    Console.WriteLine("OK");
                    return 0;
                }
                else if (args.Length == 2)
                {
                    CreateClass(args[0], args[1]);
                    Console.WriteLine("OK");
                    return 0;
                }
                else
                {
                    PrintUsage();
                    return 2;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine();
                Console.WriteLine();
                Console.WriteLine("EXCEPTION");
                Console.WriteLine("=========");
                Console.WriteLine(ex.ToString());
                return 1;
            }
        }

        private static void UpdateConfigFromSchema(string targetFileName, GenConfig genConfig)
        {
            if (!targetFileName.ToLower().EndsWith(".xml"))
                targetFileName += ".xml";
            CheckNotReadOnly(targetFileName);

            string connectionString = genConfig.DatabaseConnection.GetConnectionString();

            DataTable dt;
            using (SqlConnection conn = new SqlConnection(connectionString))
            using (SqlCommand cmd = new SqlCommand(GetStoredProcedureDataSql, conn))
            using (SqlDataAdapter da = new SqlDataAdapter(cmd))
            {
                DataSet ds = new DataSet();
                da.Fill(ds);
                dt = ds.Tables[0];
            }

            List<StoredProcedureConfig> sprocs = genConfig.StoredProcedures;

            foreach (StoredProcedureConfig testSproc in sprocs)
            {
                testSproc.Parameters.Clear();
            }

            foreach (DataRow dr in dt.Rows)
            {
                int storedProcedureID = GetInt32(dr["StoredProcedureID"]);
                string storedProcedureSchema = GetString(dr["StoredProcedureSchema"]);
                string storedProcedureName = GetString(dr["StoredProcedureName"]);
                int? parameterID = GetInt32Nullable(dr["ParameterID"]);
                string parameterName = GetString(dr["ParameterName"]);
                string parameterTypeName = GetString(dr["ParameterTypeName"]);
                int? parameterLength = GetInt32Nullable(dr["ParameterLength"]);
                //bool? parameterHasDefaultValue = GetBooleanNullable(dr["ParameterHasDefaultValue"]);
                bool? parameterIsOutputParameter = GetInt32Nullable(dr["ParameterIsOutputParameter"]) == 1 ? true : false;

                StoredProcedureConfig sproc = null;
                foreach (StoredProcedureConfig testSproc in sprocs)
                {
                    if (testSproc.ID == storedProcedureID)
                    {
                        sproc = testSproc;
                        break;
                    }
                }

                if (sproc == null)
                {
                    sproc = new StoredProcedureConfig();
                    sprocs.Add(sproc);
                }

                sproc.ID = storedProcedureID;
                sproc.Schema = storedProcedureSchema;
                sproc.Name = storedProcedureName;

                if (parameterID != null)
                {
                    ParameterConfig param = new ParameterConfig();
                    param.ID = (int)parameterID;
                    param.Name = parameterName;
                    param.DataType = parameterTypeName;
                    param.Length = parameterLength;
                    //param.HasDefaultValue = (bool)parameterHasDefaultValue;
                    param.IsOutParameter = (bool)parameterIsOutputParameter;

                    sproc.Parameters.Add(param);
                }
            }

            // get has_default_value
            foreach (StoredProcedureConfig sproc in sprocs)
            {
                // skip sprocs with no parameters
                if (sproc.Parameters.Count == 0)
                    continue;

                string sql = string.Format("select OBJECT_DEFINITION(OBJECT_ID(N'[{0}].[{1}]'))", sproc.Schema, sproc.Name);

                string objectDefinition;
                using (SqlConnection conn = new SqlConnection(connectionString))
                using (SqlCommand cmd = new SqlCommand(sql, conn))
                using (SqlDataAdapter da = new SqlDataAdapter(cmd))
                {
                    DataSet ds = new DataSet();
                    da.Fill(ds);
                    DataTable tmpdt = ds.Tables[0];
                    DataRow dr = tmpdt.Rows[0];
                    objectDefinition = GetString(dr[0]);
                }

                objectDefinition = RemoveSqlComments(objectDefinition);
                SetHasDefaultParameter(sproc, objectDefinition);
            }

            string xml;

            using (MemoryStream ms = new MemoryStream())
            {
                XmlTextWriter writer = new XmlTextWriter(ms, Encoding.ASCII);
                writer.Formatting = Formatting.Indented;

                genConfig.WriteXml(writer);

                writer.Flush();
                xml = Encoding.ASCII.GetString(ms.ToArray());
            }

            File.WriteAllText(targetFileName, xml);
        }

        private static void PrintUsage()
        {
            Console.WriteLine("usage:");
            Console.WriteLine();
            Console.WriteLine("sprocgen                               -  Create new config");
            Console.WriteLine("sprocgen [config.xml] [outputFile.cs]  -  Create .cs from config");
        }

        private static void CreateConfig()
        {
            string databaseServer = GetRequiredTextFromConsole("Database Server");
            string databaseName = GetRequiredTextFromConsole("Database Name");
            bool trustedConnection = GetYNFromConsole("Trusted Connection");
            string userName = null;
            string password = null;
            if (!trustedConnection)
            {
                userName = GetRequiredTextFromConsole("User Name");
                password = GetRequiredTextFromConsole("Password");
            }

            GenConfig genConfig = new GenConfig();
            genConfig.DatabaseConnection.Server = databaseServer;
            genConfig.DatabaseConnection.Database = databaseName;
            genConfig.DatabaseConnection.IsTrustedConnection = trustedConnection;
            if (!trustedConnection)
            {
                genConfig.DatabaseConnection.UserName = userName;
                genConfig.DatabaseConnection.Password = password;
            }

            Console.Write("Testing connection...");
            string connectionString = genConfig.DatabaseConnection.GetConnectionString();
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                conn.Open();
            }
            Console.WriteLine(" OK!");

            string gennamespace = GetTextFromConsole("Namespace", "DataAccess");
            string className = GetTextFromConsole("Class Name", "StoredProcedures");
            string cskey = GetRequiredTextFromConsole("Connection String Config Key");
            string configname;
            bool retry;
            do
            {
                retry = false;
                configname = GetTextFromConsole("Config file name", databaseName + ".xml");
                if (File.Exists(configname))
                {
                    bool overwrite = GetYNFromConsole("File exists. Overwrite?");
                    if (!overwrite)
                        retry = true;
                }
            } while (retry);

            genConfig.ClassName = className;
            genConfig.ConnectionStringKey = cskey;
            genConfig.Namespace = gennamespace;

            UpdateConfigFromSchema(configname, genConfig);

            bool createNow = GetYNFromConsole("Create C# Now?");
            if (createNow)
            {
                string fileName = GetRequiredTextFromConsole("File Name");
                CreateClass(configname, fileName);
            }
        }

        private static string GetTextFromConsole(string label, string defaultValue)
        {
            Console.Write(string.Format("{0} [{1}]: ", label, defaultValue));
            string value = (Console.ReadLine() ?? string.Empty).Trim();
            if (string.IsNullOrEmpty(value))
                return defaultValue;
            return value;
        }

        private static bool GetYNFromConsole(string label)
        {
            do
            {
                Console.Write(label + " [Y/N]: ");
                string value = (Console.ReadLine() ?? string.Empty).Trim().ToLower();
                if (value == "y" || value == "yes")
                    return true;
                if (value == "n" || value == "no")
                    return false;
            } while (true);
        }

        private static string GetRequiredTextFromConsole(string label)
        {
            string value;
            do
            {
                Console.Write(label + ": ");
                value = (Console.ReadLine() ?? string.Empty).Trim();
            } while (string.IsNullOrEmpty(value));

            return value;
        }

        private static void CreateClass(string configFile, string targetFile)
        {
            if (!targetFile.ToLower().EndsWith(".cs"))
                targetFile += ".cs";
            CheckNotReadOnly(targetFile);

            GenConfig genConfig = new GenConfig();
            XmlDocument doc = new XmlDocument();
            doc.Load(configFile);
            genConfig.ReadXml(doc);

            UpdateConfigFromSchema(configFile, genConfig);

            StringBuilder csharp = new StringBuilder();
            csharp.AppendLine("using System;");
            csharp.AppendLine("using System.Collections.Generic;");
            csharp.AppendLine("using System.Configuration;");
            csharp.AppendLine("using System.Data;");
            csharp.AppendLine("using System.Data.SqlClient;");
            csharp.AppendLine();
            csharp.AppendLine(string.Format("namespace {0}", genConfig.Namespace));
            csharp.AppendLine("{");
            csharp.AppendLine(string.Format("\tpublic static class {0}", genConfig.ClassName));
            csharp.AppendLine("\t{");
            csharp.AppendLine(string.Format("\t\tprivate const string m_ConnectionStringKey = \"{0}\";", genConfig.ConnectionStringKey));
            csharp.AppendLine("\t\tprivate static readonly string m_ConnectionString;");
            csharp.AppendLine();

            // constructor
            csharp.AppendLine(string.Format("\t\tstatic {0}()", genConfig.ClassName));
            csharp.AppendLine("\t\t{");
            csharp.AppendLine(string.Format("\t\t\tConnectionStringSettings connSettings = ConfigurationManager.ConnectionStrings[m_ConnectionStringKey];"));
            csharp.AppendLine(string.Format("\t\t\tif (connSettings == null)"));
            csharp.AppendLine(string.Format("\t\t\t\tthrow new Exception(string.Format(\"Connection string '{{0}}' not found in configuration file.\", m_ConnectionStringKey));"));
            csharp.AppendLine(string.Format("\t\t\tm_ConnectionString = connSettings.ConnectionString;"));
            csharp.AppendLine("\t\t}");
            csharp.AppendLine();

            // GetDataSet
            csharp.AppendLine("\t\tprivate static DataSet GetDataSet(string storedProcedure)");
            csharp.AppendLine("\t\t{");
            csharp.AppendLine("\t\t\treturn GetDataSet(storedProcedure, null);");
            csharp.AppendLine("\t\t}");
            csharp.AppendLine();

            csharp.AppendLine("\t\tprivate static DataSet GetDataSet(string storedProcedure, SqlParameter[] p)");
            csharp.AppendLine("\t\t{");
            csharp.AppendLine("\t\t\tusing (SqlConnection connection = new SqlConnection(m_ConnectionString))");
            csharp.AppendLine("\t\t\tusing (SqlCommand cmd = new SqlCommand(storedProcedure, connection))");
            csharp.AppendLine("\t\t\tusing (SqlDataAdapter da = new SqlDataAdapter(cmd))");
            csharp.AppendLine("\t\t\t{");
            csharp.AppendLine("\t\t\t\tcmd.CommandType = CommandType.StoredProcedure;");
            csharp.AppendLine("\t\t\t\tif (p != null && p.Length != 0)");
            csharp.AppendLine("\t\t\t\t\tcmd.Parameters.AddRange(p);");
            csharp.AppendLine("\t\t\t\tDataSet ds = new DataSet();");
            csharp.AppendLine("\t\t\t\tda.Fill(ds);");
            csharp.AppendLine();
            csharp.AppendLine("\t\t\t\treturn ds;");
            csharp.AppendLine("\t\t\t}");
            csharp.AppendLine("\t\t}");
            csharp.AppendLine();

            StringBuilder parameterClasses = new StringBuilder();

            foreach (StoredProcedureConfig sproc in genConfig.StoredProcedures)
            {
                if (!sproc.Generate)
                    continue;

                bool canGenerate = true;
                bool hasDefaultParameters = false;
                StringBuilder parameters = new StringBuilder();
                StringBuilder methodBody = new StringBuilder();
                StringBuilder paramClass = new StringBuilder();
                StringBuilder paramClassConstructorBody = new StringBuilder();

                string methodName;
                if (sproc.Schema == "dbo")
                    methodName = sproc.Name;
                else
                    methodName = string.Format("{0}_{1}", sproc.Schema, sproc.Name);

                string paramClassName = string.Format("{0}Parameters", methodName);

                paramClass.AppendLine(string.Format("\tpublic class {0}", paramClassName));
                paramClass.AppendLine("\t{");

                int requiredParamCount = 0;
                foreach (ParameterConfig param in sproc.Parameters)
                {
                    if (param.IsOutParameter)
                    {
                        canGenerate = false;
                        break;
                    }

                    if (param.HasDefaultValue)
                    {
                        hasDefaultParameters = true;

                        paramClass.AppendLine(string.Format("\t\tprivate bool m_Is{0}Set;", param.PascalName));
                        paramClass.AppendLine(string.Format("\t\tprivate {0} m_{1};", param.FriendlyDataType, param.PascalName));
                        paramClass.AppendLine(string.Format("\t\tpublic {0} {1}", param.FriendlyDataType, param.PascalName));
                        paramClass.AppendLine("\t\t{");
                        paramClass.AppendLine(string.Format("\t\t\tget {{ return m_{0}; }}", param.PascalName));
                        paramClass.AppendLine("\t\t\tset");
                        paramClass.AppendLine("\t\t\t{");
                        paramClass.AppendLine(string.Format("\t\t\t\tm_Is{0}Set = true;", param.PascalName));
                        paramClass.AppendLine(string.Format("\t\t\t\tm_{0} = value;", param.PascalName));
                        paramClass.AppendLine("\t\t\t}");
                        paramClass.AppendLine("\t\t}");
                        paramClass.AppendLine(string.Format("\t\tpublic bool Is{0}Set", param.PascalName));
                        paramClass.AppendLine("\t\t{");
                        paramClass.AppendLine(string.Format("\t\t\tget {{ return m_Is{0}Set; }}", param.PascalName));
                        paramClass.AppendLine("\t\t}");
                        paramClass.AppendLine();
                    }
                    else
                    {
                        if (parameters.Length != 0)
                            parameters.Append(", ");

                        parameters.Append(string.Format("{0} {1}", param.FriendlyDataType, param.CamelName));

                        methodBody.AppendLine(string.Format("\t\t\tp[{0}] = new SqlParameter(\"@{1}\", SqlDbType.{2});", requiredParamCount, param.CamelName, param.SqlDbType));
                        methodBody.AppendLine(string.Format("\t\t\tp[{0}].Value = (object){1} ?? DBNull.Value;", requiredParamCount, param.CamelName));

                        requiredParamCount++;

                        paramClass.AppendLine(string.Format("\t\tprivate readonly {0} m_{1};", param.FriendlyDataType, param.PascalName));
                        paramClass.AppendLine(string.Format("\t\tpublic {0} {1}", param.FriendlyDataType, param.PascalName));
                        paramClass.AppendLine("\t\t{");
                        paramClass.AppendLine(string.Format("\t\t\tget {{ return m_{0}; }}", param.PascalName));
                        paramClass.AppendLine("\t\t}");
                        paramClass.AppendLine();

                        paramClassConstructorBody.AppendLine(string.Format("\t\t\tm_{0} = {1};", param.PascalName, param.CamelName));
                    }
                }

                if (!canGenerate)
                    continue;

                if (hasDefaultParameters)
                {
                    if (requiredParamCount > 0)
                    {
                        paramClass.AppendLine(string.Format("\t\tpublic {0}({1})", paramClassName, parameters));
                        paramClass.AppendLine("\t\t{");
                        paramClass.AppendLine(paramClassConstructorBody.ToString());
                        paramClass.AppendLine("\t\t}");
                    }
                    paramClass.AppendLine("\t}"); // end class

                    parameterClasses.AppendLine(paramClass.ToString());
                }

                StringBuilder method = new StringBuilder();

                method.AppendLine(string.Format("\t\tpublic static DataSet {0}({1})", methodName, parameters));
                method.AppendLine("\t\t{");
                if (requiredParamCount > 0)
                {
                    method.AppendLine(string.Format("\t\t\tSqlParameter[] p = new SqlParameter[{0}];", requiredParamCount));
                    method.AppendLine(methodBody.ToString());
                    method.AppendLine(string.Format("\t\t\treturn GetDataSet(\"[{0}].[{1}]\", p);", sproc.Schema, sproc.Name));
                }
                else
                {
                    method.AppendLine(string.Format("\t\t\treturn GetDataSet(\"[{0}].[{1}]\");", sproc.Schema, sproc.Name));
                }
                method.AppendLine("\t\t}");
                method.AppendLine();

                // stored procedure method overload with optional parameters
                if (hasDefaultParameters)
                {
                    method.AppendLine(string.Format("\t\tpublic static DataSet {0}({1} parameters)", methodName, paramClassName));
                    method.AppendLine("\t\t{");

                    method.AppendLine("\t\t\tList<SqlParameter> pList = new List<SqlParameter>();");
                    //method.AppendLine("\t\t\tSqlParameter p;");
                    bool isFirstAssignment = true;
                    foreach (ParameterConfig param in sproc.Parameters)
                    {
                        if (!param.HasDefaultValue)
                        {
                            method.AppendLine(string.Format("\t\t\t{2}p = new SqlParameter(\"{0}\", SqlDbType.{1});", param.Name, param.SqlDbType, isFirstAssignment ? "SqlParameter " : ""));
                            method.AppendLine(string.Format("\t\t\tp.Value = (object)parameters.{0} ?? DBNull.Value;", param.PascalName));
                            method.AppendLine(string.Format("\t\t\tpList.Add(p);"));
                            isFirstAssignment = false;
                        }
                    }
                    foreach (ParameterConfig param in sproc.Parameters)
                    {
                        if (param.HasDefaultValue)
                        {
                            method.AppendLine(string.Format("\t\t\tif (parameters.Is{0}Set)", param.PascalName));
                            method.AppendLine("\t\t\t{");
                            method.AppendLine(string.Format("\t\t\t\t{2}p = new SqlParameter(\"{0}\", SqlDbType.{1});", param.Name, param.SqlDbType, isFirstAssignment ? "SqlParameter " : ""));
                            method.AppendLine(string.Format("\t\t\t\tp.Value = (object)parameters.{0} ?? DBNull.Value;", param.PascalName));
                            method.AppendLine(string.Format("\t\t\t\tpList.Add(p);"));
                            method.AppendLine("\t\t\t}");
                        }
                    }

                    method.AppendLine(string.Format("\t\t\treturn GetDataSet(\"[{0}].[{1}]\", pList.ToArray());", sproc.Schema, sproc.Name));
                    method.AppendLine("\t\t}");
                    method.AppendLine();
                }

                csharp.AppendLine(method.ToString());
            }

            csharp.AppendLine("\t}"); // end class

            csharp.AppendLine();
            csharp.AppendLine("\t#region <<< Parameter Classes >>>");
            csharp.AppendLine();
            csharp.AppendLine(parameterClasses.ToString());
            csharp.AppendLine();
            csharp.AppendLine("\t#endregion <<< Parameter Classes >>>");
            csharp.AppendLine();

            csharp.AppendLine("}"); // end namespace

            csharp = new StringBuilder(CleanUpCSharp(csharp.ToString()));

            File.WriteAllText(targetFile, csharp.ToString());
        }

        private static void CheckNotReadOnly(string targetFile)
        {
            if (File.Exists(targetFile))
            {
                FileInfo info = new FileInfo(targetFile);
                if ((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                {
                    throw new IOException(string.Format("File '{0}' is read only.", targetFile));
                }
            }
        }

        private static void SetHasDefaultParameter(StoredProcedureConfig sproc, string objectDefinition)
        {
            string updef = objectDefinition.ToUpper();
            bool repeat;
            int asidx = updef.IndexOf("AS");
            do
            {
                repeat = true;
                if (updef[asidx - 1] == ' ' && updef[asidx + 2] == ' ')
                    repeat = false;
                else if (updef[asidx - 1] == ' ' && updef[asidx + 2] == '\r')
                    repeat = false;
                else if (updef[asidx - 1] == '\n' && updef[asidx + 2] == '\r')
                    repeat = false;
                else if (updef[asidx - 1] == '\n' && updef[asidx + 2] == ' ')
                    repeat = false;
                else
                    asidx = updef.IndexOf("AS", asidx + 1);
            } while (repeat);

            updef = updef.Substring(0, asidx);

            foreach (ParameterConfig param in sproc.Parameters)
            {
                int idx = updef.IndexOf(param.Name.ToUpper());
                int endIdx = updef.IndexOf(',', idx);
                if (endIdx == -1)
                    endIdx = updef.Length;
                int startIdx = idx + param.Name.Length;
                string mid = updef.Substring(startIdx, endIdx - startIdx);
                if (mid.Contains("="))
                {
                    param.HasDefaultValue = true;
                }
                else
                {
                    param.HasDefaultValue = false;
                }
            }
        }

        private static string RemoveSqlComments(string objectDefinition)
        {
            List<string> lines = new List<string>(objectDefinition.Split(new string[] { Environment.NewLine }, StringSplitOptions.None));
            bool isInComment = false;
            for (int i = 0; i < lines.Count; i++)
            {
                string line = lines[i];

                line = line.Replace("\t", "    ");

                // preserve empty lines
                if (!isInComment && string.IsNullOrEmpty(line.Replace("\t", "").Trim()))
                {
                    lines[i] = string.Empty;
                    continue;
                }

                // doesn't account for comment-like patterns in strings

                bool repeat;
                do
                {
                    repeat = false;
                    int idx = line.IndexOf("/*");
                    if (isInComment)
                        idx = 0;
                    if (idx != -1)
                    {
                        isInComment = true;
                        int endIdx = line.IndexOf("*/");
                        if (endIdx != -1)
                        {
                            line = line.Substring(0, idx) + line.Substring(endIdx + 2);
                            repeat = true;
                            isInComment = false;
                        }
                        else
                        {
                            line = line.Substring(0, idx);
                        }
                    }
                } while (repeat);

                if (!isInComment)
                {
                    if (line.Contains("--"))
                    {
                        int idx = line.IndexOf("--");
                        line = line.Substring(0, idx);
                    }
                }

                line = line.TrimEnd(' ');
                if (string.IsNullOrEmpty(line))
                {
                    lines.RemoveAt(i);
                    i--;
                }
                else
                {
                    lines[i] = line;
                }
            }

            StringBuilder sb = new StringBuilder();
            foreach (string line in lines)
                sb.AppendLine(line);
            return sb.ToString();
        }

        private static string CleanUpCSharp(string code)
        {
            string[] split = new string[1];
            split[0] = Environment.NewLine;

            string[] lines = code.Split(split, StringSplitOptions.None);
            StringBuilder newLines = new StringBuilder();

            bool lastLineIsBlank = false;
            bool lastLineIsOpenBrace = false;

            foreach (string line in lines)
            {
                bool lineIsBlank = false;
                bool lineIsCloseBrace = false;
                bool lineIsOpenBrace = false;

                string modLine = line.Replace("\t", "").Replace(" ", "");
                if (string.IsNullOrEmpty(modLine))
                {
                    lineIsBlank = true;
                }
                else
                {
                    if (modLine == "{")
                        lineIsOpenBrace = true;
                    else if (modLine == "}")
                        lineIsCloseBrace = true;
                }

                if (lineIsBlank && lastLineIsBlank)
                {
                    // don't output line
                }
                else if (lineIsCloseBrace && lastLineIsBlank)
                {
                    // remove last line
                    newLines.Remove(newLines.Length - 2, 2);
                    newLines.AppendLine(line.Replace("\t", "    ").TrimEnd(' '));
                }
                else if (lineIsBlank && lastLineIsOpenBrace)
                {
                    // don't output line

                    // set to previous line state
                    lineIsOpenBrace = true;
                    lineIsBlank = false;
                }
                else
                {
                    newLines.AppendLine(line.Replace("\t", "    ").TrimEnd(' '));
                }

                lastLineIsBlank = lineIsBlank;
                lastLineIsOpenBrace = lineIsOpenBrace;
            }

            return newLines.ToString();
        }
    }
}
